import os
import time
import datetime
import simplejson
import logging
import itertools
import operator
from google.appengine.ext.webapp import template
from google.appengine.api import taskqueue
from google.appengine.api import memcache
from google.appengine.ext import webapp
from google.appengine.api import users
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext import db
from google.appengine.ext.db import stats
from scrape import *
from db import *
from combinatorics import *

# Set to true if we want to have our webapp print stack traces, etc
_DEBUG = True

MAX_NUM_SCHEDULES = 25

logging.getLogger().setLevel(logging.DEBUG)

class ExpectedError(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)

## Basic request handler for all pages.
#  @param webapp.RequestHandler Defines to AppEngine that this method will handle incoming requests
class BaseRequestHandler(webapp.RequestHandler):
    """Supplies a common template generation function.

    When you call generate(), we augment the template variables supplied with
    the current user in the 'user' variable and the current webapp request
    in the 'request' variable.
    """
    def generate(self, template_name, template_values={}):
        values = {
                'request': self.request,
                'user': users.GetCurrentUser(),
                'login_url': users.create_login_url("/"),
                'logout_url': users.create_logout_url("/"),
                'debug': self.request.get('deb'),
                'application_name': 'Task Manager',
        }
        values.update(template_values)
        directory = os.path.dirname(__file__)
        path = os.path.join(directory, template_name)
        self.response.out.write(template.render(path, values, debug=_DEBUG))

## Landing page request handler, redirecting to the schedule page if the user is already logged in, else to the home page.
class MainPage(BaseRequestHandler):
    def get(self):
        if users.get_current_user():
            self.redirect("/schedule")
        else:
            self.generate('index.html')

## Schedule page request handler.
class SchedulePage(BaseRequestHandler):
    def get(self):
        error_message = self.request.get("error_message")

        user = User.all().filter("user =", users.get_current_user()).get()

        # Redirect the user to fill out profile info, if they haven't done so yet.
        if(user == None):
            self.redirect("/profile")
            return

        courses = map(lambda x: {'name': db.get(x).dept.code + db.get(x).courseNum, 'key': str(x)}, user.selectedCourses)
        
        def timeToStr(time):
            hour = 12 if time.hour == 12 else (time.hour % 12)
            am_pm = "pm" if time.hour >= 12 else "am"
            return str(hour) + ":" + ("%02d" % time.minute) + am_pm        
        
        constraints = [{
            'name': offsetToFullDayMap[int(x.day)] + ": " + timeToStr(x.startTime) + " - " + timeToStr(x.endTime),
            'id': i
        } for i,x in enumerate(user.selectedConstraints)]
        
        departments = Department.all()
        departments = map(lambda x: x.code, departments)

        self.generate('schedule.html', {
                'error_message': error_message,
                'courses': courses,
                'constraints': constraints,
                'departments': departments
        })

    def post(self):
        user = User.all().filter("user =", users.get_current_user()).get()

        if(user == None):
            self.redirect("/profile")
            return

        action = self.request.get("action")

        courses = []
        try:
            if action == "addCourse":
                dept = self.request.get("department")
                num = self.request.get("coursenum")

                if(dept == "" or num == ""):
                    raise ExpectedError("You must specify both a department and course number.")

                dept = Department.all().filter("code =", dept).get()
                
                logging.info(dept)

                q = Course.all()
                q.filter("dept =", dept)
                q.filter("courseNum = ", num)
                course = q.get()
                if course == None:
                    raise ExpectedError("The course you entered is not available during the upcoming semester.")

                for existing_course in user.selectedCourses:
                    if(db.get(existing_course).key() == course.key()):
                        raise ExpectedError("The course you entered has already been added.")

                user.selectedCourses.append(course.key());

                user.put()
            elif action == "deleteCourse":
                key = self.request.get("key")
                key = db.Key(key)
                user.selectedCourses.remove(key)
                user.put()
            elif action == "addConstraint":
                startTime = self.request.get("startTime")
                endTime = self.request.get("endTime")
                day = self.request.get("day")
                
                if(startTime == "" or endTime == "" or day == ""):
                    raise ExpectedError("You must specify a day, start time and end time.")

                day = int(day)
                startTime = datetime.datetime.strptime(startTime, "%I:%M %p").time()
                endTime = datetime.datetime.strptime(endTime, "%I:%M %p").time()     
                    
                constraint = TimeRange(startTime, endTime, day)
                
                user.selectedConstraints.append(constraint)
                user.put()
            elif action == "deleteConstraint":
                id = self.request.get("id")
                id = int(id)
                user.selectedConstraints.pop(id)
                user.put()
        except ExpectedError, e:
            self.redirect('/schedule?error_message=%s' % e.value)
            return
            
        self.redirect('/schedule')

## Profile page request handler
class ProfilePage(BaseRequestHandler):
    ## Generates the profile page using user values if logged in previously.
    def get(self):
        user = User.all().filter("user =", users.get_current_user()).get()
        template_values = {
            'error_message': self.request.get("error_message")
        }

        if(user != None):
            if user.firstName != None: template_values['firstName'] = user.firstName
            if user.lastName != None: template_values['lastName'] = user.lastName
            if user.college != None: template_values['college'] = user.college
            if user.major != None: template_values['major'] = user.major
            if user.minor != None: template_values['minor'] = user.minor
            if user.track != None: template_values['track'] = user.track            
            if user.graduationYear != None: template_values['graduationYear'] = str(user.graduationYear.year)

        self.generate('profile.html', template_values)

    ## Upon submitting the page, file the input into the user's data, handling any errors that may arise in input.
    def post(self):
        user = User.all().filter("user =", users.get_current_user()).get()

        if(user == None):
            user = User()
            user.user = users.get_current_user()

        firstName = self.request.get("firstName")
        lastName = self.request.get("lastName")
        college = self.request.get("college")
        major = self.request.get("major")
        minor = self.request.get("minor")
        track = self.request.get("track")
        graduationYear = self.request.get("graduationYear")

        try:
            if(len(firstName) > 0): user.firstName = firstName
            else:
                raise ExpectedError("First and last name required.")
            if(len(lastName) > 0): user.lastName = lastName
            else:
                raise ExpectedError("First and last name required.")
            if(len(graduationYear) > 0): user.graduationYear = datetime.date(year=int(graduationYear), month=1, day=1)
            user.college = college
            user.major = major
            user.minor = minor
            user.track = track

            user.put()

        except ExpectedError, e:
            self.redirect('/profile?error_message=%s' % e.value)
            return

        self.redirect('/schedule')

## Schedule results page, directed to from the schedule page.
class ScheduleResultsPage(BaseRequestHandler):
    def get(self):
        user = User.all().filter("user =", users.get_current_user()).get()

        # Redirect the user to fill out profile info, if they haven't done so yet.
        if(user == None):
            self.redirect("/profile")
            return        
        
        schedule = self.request.get("schedule")
        if schedule == "":
            self.redirect('schedule_results?schedule=0')
            return
        else:
            schedule = int(schedule)
            
        q = user.schedules.order("-weightedTotal")

        # this is too slow... need to put the results in memcache
        results = q.fetch(1, offset=schedule)
        item = None
        for result in results: item = result
            
        logging.info(schedule);
        logging.info((user.numSchedules-1));
        
        if schedule > (user.numSchedules-1) or schedule >= MAX_NUM_SCHEDULES:
            redirect_schedule = min(MAX_NUM_SCHEDULES-1, user.numSchedules-1)
            self.redirect('schedule_results?schedule=%d' % redirect_schedule)
            return
        
        templateValues = {
            'error_message': self.request.get("error_message"),
            'cur_schedule': str(schedule+1),
            'score': str(item.weightedTotal / len(item.courses))
        }
        
        logging.info(schedule)
        
        if schedule > 0:
            templateValues['prev_schedule'] = str(schedule-1)
            
        if schedule < (user.numSchedules-1) and schedule < (MAX_NUM_SCHEDULES-1):
            templateValues['next_schedule'] = str(schedule+1)
        
        self.generate('schedule_results.html', templateValues)

    # return true if there is a time intersection.
    # two times are not considers to intersect if a boundary touches
    def has_time_intersection(self, t1, t2):
        t1start, t1end = t1[0], t1[1]
        t2start, t2end = t2[0], t2[1]
        return  (t1start <= t2start and t2start < t1end) or \
                (t2start <= t1start and t1start < t2end)

    def has_conflict(self, courses, constraints):
        for i in range(1, len(courses)):
            for j in range(i):
                times1 = courses[i].times
                times2 = courses[j].times

                for time1 in times1:                    
                    for time2 in times2:
                        if time1.day == time2.day:
                            if self.has_time_intersection((time1.startTime, time1.endTime), (time2.startTime, time2.endTime)) == True:
                                return True

        return False

    def post(self):
        user = User.all().filter("user =", users.get_current_user()).get()

        # Redirect the user to fill out profile info, if they haven't done so yet.
        if(user == None):
            self.redirect("/profile")
            return

        num_courses = self.request.get("numcourses")

        try:
            if(len(num_courses) < 0):
                raise ExpectedError("You need to specify the number of courses to be taken.")

            num_courses = int(num_courses)

            logging.info(str(num_courses))

            query = CourseSchedule.all().filter("user =", user)
            db.delete(query)

            # need to change the way that courses are stored by /schedule to just be a list of strings...
            courses = map(lambda x: (db.get(x).dept, db.get(x).courseNum), user.selectedCourses)
            constraints = user.selectedConstraints

            mCourses = {}
            for (dept, courseNum) in courses:
                q = Course.all()
                q.filter("courseNum =", courseNum)
                q.filter("dept = ", dept)
                sections = q.fetch(100)
                mCourses[dept.code + courseNum] = sections
            
            def check_constraint_conflicts(course):
                for constraint in constraints:
                    for time in course.times:
                        if (constraint.day == -1) or (time.day == constraint.day):
                            if (self.has_time_intersection((time.startTime, time.endTime), (constraint.startTime, constraint.endTime)) == True):
                                return True
                return False

            for (key,val) in mCourses.iteritems():
                mCourses[key] = [x for x in val if not check_constraint_conflicts(x)]

            if num_courses > len(mCourses):
                raise ExpectedError("You need to specify a course load less than or equal to the number of possible courses.")
                
            combinations = combinate(mCourses, num_courses)

            i=j=0
            schedules = []
            for combination in combinations:
                if self.has_conflict(combination, constraints) == False:
                    rmpTotal = 0.0
                    myumTotal = 0.0
                    weightedTotal = 0.0
                    for section in combination:
                        if section.professor.myumRatingOverall != None:
                            myumTotal += section.professor.myumRatingOverall
                        if section.professor.rateMyProfessorOverallRating != None:
                            rmpTotal += section.professor.rateMyProfessorOverallRating
                        if section.professor.weightedRating != None:
                            weightedTotal += section.professor.weightedRating

                    schedule = CourseSchedule()
                    schedule.user = user
                    schedule.courses = map(lambda x: x.key(), combination)
                    schedule.rmpTotal = rmpTotal
                    schedule.myumTotal = myumTotal
                    schedule.weightedTotal = weightedTotal
                    schedules.append(schedule)

                    j += 1
                i += 1
            
            if i <= 0:
                raise ExpectedError("No possible schedules were found matching your criteria.")
            
            # sort the schedules in advance, and only put the top 25 into the datastore
            # the biggest bottlenecks are dealing with adding/removing 12000 Schedules,
            # not the algorithm runtime
            schedules = sorted(schedules, key=operator.attrgetter('weightedTotal'), reverse=True)
            del schedules[MAX_NUM_SCHEDULES:]
            #logging.info(schedules)
            
            # doing a batch put is much faster than doing a .put() in the loop
            db.put(schedules)

            logging.info("Got " + str(i) + " combinations.")
            logging.info("Got " + str(j) + " no-conflict combinations.")
            
            user.numSchedules = j
            user.put()

        except ExpectedError, e:
            self.redirect('/schedule?error_message=%s' % e.value)
            return

        self.redirect('/schedule_results')

## AJAX request to generate schedule requests for the ScheduleResults page.
class GetSchedule(webapp.RequestHandler):
    ## Convert courses from database to JSON data for the calendar.
    #  @param id Identification for each course (position) in the JSON data.
    #  @param course Course to convert to entry.
    def courseToEntries(self, id, course):
        today = datetime.datetime.today()
        mon = today - datetime.timedelta(days=today.weekday())

        entries = []
        for time in course.times:
            #time = db.get(time)
            
            date = datetime.datetime(mon.year, mon.month, mon.day + time.day).date()

            body = course.professor.firstName + ". " + course.professor.lastName + "\n" \
                + "\nRateMyProfessors Score: " + str(course.professor.rateMyProfessorOverallRating) \
                + "\nMyUM Score: " + str(course.professor.myumRatingOverall) \
                + "\nCanesCalendar Score: " + str(course.professor.weightedRating)


            entries.append({
                "id": id,
                "title": course.dept.code + course.courseNum + " " + course.section,
                "start": datetime.datetime.combine(date, time.startTime),
                "end": datetime.datetime.combine(date, time.endTime),
                "rmpScore": course.professor.rateMyProfessorOverallRating,
                "myumScore": course.professor.myumRatingOverall,
                "weightedScore": course.professor.weightedRating,
                "professor": course.professor.firstName + ". " + course.professor.lastName
            })
            id += 1

        return entries

    ## Handle a request to get a new schedule. Called by ScheduleResults page.
    def get(self):
        schedule = self.request.get("schedule")
        # catch exception for non-numbers
        schedule = int(schedule)

        user = User.all().filter("user =", users.get_current_user()).get()

        # Redirect the user to fill out profile info, if they haven't done so yet.
        if(user == None):
            self.redirect("/profile")
            return

        q = user.schedules.order("-weightedTotal")

        # this is too slow... need to put the results in memcache
        results = q.fetch(1, offset=schedule)
        item = None
        for result in results: item = result
        
        logging.info(item)

        maxClassDay = 0
        events = []
        id = 1
        for section in item.courses:
            section = db.get(section)            
            
            for time in section.times:
                if time.day > maxClassDay:
                    maxClassDay = time.day
            
            events.extend(self.courseToEntries(id, section))
            id += len(section.times)
            
        # only display saturday or sunday if necessary - other wise show monday through friday
        if maxClassDay < 4:
            maxClassDay = 4

        dthandler = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) else None
        self.response.out.write(simplejson.dumps({"events": events, "options": {"daysToShow": maxClassDay+1}}, default=dthandler))

## Search page request handler.
class SearchPage(BaseRequestHandler):
    ## Show results of search page. Name taken in as input and search initiated. Results of the search will be displayed on a new Search page.
    def get(self):
        resultTable= []
        myResult = ""
        numResults = 0
        selectedProf = False
        selectedCourse = False
        name = self.request.get('searchTerm')
        type = self.request.get('type')
        sort = self.request.get('sort')
        
        if name == "" or type == "":
            self.generate('search.html')
        else:
            if len(name) < 3:
                if type == 'course':
                    selectedCourse = True
                else :
                    selectedProf = True
            elif type == 'course':
                selectedCourse = True
                searchTerm = name.strip()
                searchTerm = searchTerm.upper()
               
                qDep = Department.all()
                qDep.filter("code =", searchTerm[:3])
                department = qDep.fetch(limit=1)
                if len(department) > 0:
                    department = department[0]
                    qCourse = Course.all()
                    qCourse.filter("dept =", department)
                    if len(searchTerm) > 3 :
                        qCourse.filter("courseNum = ", searchTerm[-3:])
                    courses = qCourse.fetch(limit=400)
                    
                    if sort:
                        courses = sorted(courses, key=lambda course: course.professor.weightedRating, reverse=True)
                    
                    numResults = len(courses)
                    if  not (numResults == 0):
                        for row in courses:
                            resultTable.append(row)
                        
            elif type == 'professor':
                selectedProf = True
                searchTerm = name.strip()
                keywords = searchTerm.split()
                results = []
                for prof in keywords:
                    prof = prof[0].upper() + prof[1:].lower()
                    qProf = Professor.all()
                    qProf.filter("lastName >=", prof).filter("lastName <", unicode(prof) + u"\ufffd")
                    if sort:
                        qProf.order("-lastName").order("-weightedRating")
                    queryProf = qProf.fetch(limit=400)
                    
                    logging.info(qProf.get())
                    
                    if len(queryProf) > 0:
                        for p in queryProf:
                            results.append(p)
                numResults = len(results)
                if not (numResults == 0):
                    for tempProf in results:
                        # courseList = ""
                        # for tempCourse in tempProf.courses:
                            # courseList += str(tempCourse) + ","
                        resultTable.append(tempProf)
        
            templateValues = {
              'selectedProf' : selectedProf,
              'selectedCourse' : selectedCourse,
              'term' : name,
              'numResults' : numResults,
              'searchResult' : resultTable,
              'sort' : sort
            }
            self.generate('search.html', templateValues)


## Settings page request handler.
class SettingsPage(BaseRequestHandler):
    def get(self):
        self.generate('settings.html')

class Counter(db.Model):
    count = db.IntegerProperty(indexed=False)


def deleteAllEntities(model): 
    q = db.GqlQuery("SELECT * FROM " + model)
    results = q.fetch(1000)
    while len(results) > 0: 
        db.delete(results)
        q = db.GqlQuery("SELECT * FROM " + model) 
        results = q.fetch(1000)

class HelpPage(BaseRequestHandler):
    def get(self):
        self.generate('help.html')    

## Admin page request handler. Used for running scraping methods.
class AdminPage(BaseRequestHandler):
    def updateStats(self):
        stat = Statistics().all().get()
        if stat == None: stat = Statistics()
        
        stat.numProfessors = Professor.all(keys_only=True).count(limit=None)
        stat.numCourses = Course.all(keys_only=True).count(limit=None)
        stat.numDepartments = Department.all(keys_only=True).count(limit=None)
        stat.put()
        
        return stat
    
    def get(self):
        try:
            if users.is_current_user_admin():
                stat = Statistics().all().get()
                # if there is no statistics object present (e.g. if the datastore has been loaded
                # fresh using bulkloader.py) then count the objects manually. this takes a while.
                if stat == None:
    			    stat = self.updateStats()
        	    
                self.generate('admin.html', {
                    'numProfessors': stat.numProfessors,
                    'numCourses': stat.numCourses,
                    'numDepartments': stat.numDepartments,
                })
            else:
                raise ExpectedError("You're not an admin...")
        except ExpectedError, e:
            self.redirect('/')
            return

    def post(self):
        action = self.request.get('action')
        
        if action == 'scrape':        
            alphabet = map(chr, range(65, 91))
            for letter in alphabet:
                taskqueue.add(
                    url = '/worker/write_behind',
                    params= {
                        'group1': self.request.get('group1'), 
                        'letter': letter,
                        'username': self.request.get('username'),
                        'password': self.request.get('password')
                    }
                )
        elif action == 'clear':
            model = self.request.get('model')
            deleteAllEntities(model)
            
            stat = Statistics().all().get()
            if stat:
                if model == "Professor": stat.numProfessors = 0
                if model == "Course": stat.numCourses = 0
                if model == "Department": stat.numDepartments = 0
                stat.put()
        elif action == 'updateStats':
            self.updateStats()
            
        self.redirect('/admin')

## Scraper task_queue worker.
class ScraperWriteBehindWorker(webapp.RequestHandler):
    def post(self):
        scraper = self.request.get('group1')
        logging.error('scraper value ' + scraper)
        if(scraper == 'rmp'):
            scrapeRMP(self.request.get('letter'))
        elif(scraper == 'myumrate'):
            scrapeUMRatings(self.request.get('username'), self.request.get('password'), self.request.get('letter'))
        elif(scraper == 'myumcourse'):
            scrapeUMCourse('20121', self.request.get('letter'))

## Database initializer.
class InitDBScrape(webapp.RequestHandler):
    def get(self):
        #taskqueue.add(scrapeUMCourse("20121"))
        #taskqueue.add(scrapeUMRatings("username","password"))
        scrapeRMP('P')
        self.response.out.write("Test values inserted")

application = webapp.WSGIApplication([
    ('/', MainPage),
    ('/profile', ProfilePage),
    ('/schedule', SchedulePage),
    ('/schedule_results', ScheduleResultsPage),
    ('/get_schedule', GetSchedule),
    ('/search', SearchPage),
    ('/settings', SettingsPage),
    ('/help', HelpPage),    
    ('/initdbscrape', InitDBScrape),
    (r'/admin', AdminPage),
    (r'/worker/write_behind', ScraperWriteBehindWorker),
    ], debug=_DEBUG)

def real_main():
    run_wsgi_app(application)
    
def profile_main():
    # This is the main function for profiling
    # We've renamed our original main() above to real_main()
    import cProfile, pstats
    prof = cProfile.Profile()
    prof = prof.runctx("real_main()", globals(), locals())
    print "<pre>"
    stats = pstats.Stats(prof)
    stats.sort_stats("time")  # Or cumulative
    stats.print_stats(80)  # 80 = how many to print
    # The rest is optional.
    # stats.print_callees()
    # stats.print_callers()
    print "</pre>"    

main = real_main

if __name__ == "__main__":
    main()